// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/proxy/dhcp_proxy_script_adapter_fetcher_win.h"

#include "base/run_loop.h"
#include "base/synchronization/waitable_event.h"
#include "base/test/test_timeouts.h"
#include "base/threading/sequenced_worker_pool.h"
#include "base/timer/elapsed_timer.h"
#include "base/timer/timer.h"
#include "net/base/net_errors.h"
#include "net/base/test_completion_callback.h"
#include "net/proxy/mock_proxy_script_fetcher.h"
#include "net/proxy/proxy_script_fetcher_impl.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace net {

namespace {

    const char kPacUrl[] = "http://pacserver/script.pac";

    // In net/proxy/dhcp_proxy_script_fetcher_win_unittest.cc there are a few
    // tests that exercise DhcpProxyScriptAdapterFetcher end-to-end along with
    // DhcpProxyScriptFetcherWin, i.e. it tests the end-to-end usage of Win32
    // APIs and the network.  In this file we test only by stubbing out
    // functionality.

    // Version of DhcpProxyScriptAdapterFetcher that mocks out dependencies
    // to allow unit testing.
    class MockDhcpProxyScriptAdapterFetcher
        : public DhcpProxyScriptAdapterFetcher {
    public:
        explicit MockDhcpProxyScriptAdapterFetcher(
            URLRequestContext* context,
            scoped_refptr<base::TaskRunner> task_runner)
            : DhcpProxyScriptAdapterFetcher(context, task_runner)
            , dhcp_delay_(base::TimeDelta::FromMilliseconds(1))
            , timeout_(TestTimeouts::action_timeout())
            , configured_url_(kPacUrl)
            , fetcher_delay_ms_(1)
            , fetcher_result_(OK)
            , pac_script_("bingo")
        {
        }

        void Cancel() override
        {
            DhcpProxyScriptAdapterFetcher::Cancel();
            fetcher_ = NULL;
        }

        ProxyScriptFetcher* ImplCreateScriptFetcher() override
        {
            // We don't maintain ownership of the fetcher, it is transferred to
            // the caller.
            fetcher_ = new MockProxyScriptFetcher();
            if (fetcher_delay_ms_ != -1) {
                fetcher_timer_.Start(FROM_HERE,
                    base::TimeDelta::FromMilliseconds(fetcher_delay_ms_),
                    this, &MockDhcpProxyScriptAdapterFetcher::OnFetcherTimer);
            }
            return fetcher_;
        }

        class DelayingDhcpQuery : public DhcpQuery {
        public:
            explicit DelayingDhcpQuery()
                : DhcpQuery()
                , test_finished_event_(
                      base::WaitableEvent::ResetPolicy::MANUAL,
                      base::WaitableEvent::InitialState::NOT_SIGNALED)
            {
            }

            std::string ImplGetPacURLFromDhcp(
                const std::string& adapter_name) override
            {
                base::ElapsedTimer timer;
                test_finished_event_.TimedWait(dhcp_delay_);
                return configured_url_;
            }

            base::WaitableEvent test_finished_event_;
            base::TimeDelta dhcp_delay_;
            std::string configured_url_;

        private:
            ~DelayingDhcpQuery() override { }
        };

        DhcpQuery* ImplCreateDhcpQuery() override
        {
            dhcp_query_ = new DelayingDhcpQuery();
            dhcp_query_->dhcp_delay_ = dhcp_delay_;
            dhcp_query_->configured_url_ = configured_url_;
            return dhcp_query_.get();
        }

        // Use a shorter timeout so tests can finish more quickly.
        base::TimeDelta ImplGetTimeout() const override { return timeout_; }

        void OnFetcherTimer()
        {
            // Note that there is an assumption by this mock implementation that
            // DhcpProxyScriptAdapterFetcher::Fetch will call ImplCreateScriptFetcher
            // and call Fetch on the fetcher before the message loop is re-entered.
            // This holds true today, but if you hit this DCHECK the problem can
            // possibly be resolved by having a separate subclass of
            // MockProxyScriptFetcher that adds the delay internally (instead of
            // the simple approach currently used in ImplCreateScriptFetcher above).
            DCHECK(fetcher_ && fetcher_->has_pending_request());
            fetcher_->NotifyFetchCompletion(fetcher_result_, pac_script_);
            fetcher_ = NULL;
        }

        bool IsWaitingForFetcher() const
        {
            return state() == STATE_WAIT_URL;
        }

        bool WasCancelled() const
        {
            return state() == STATE_CANCEL;
        }

        void FinishTest()
        {
            DCHECK(dhcp_query_.get());
            dhcp_query_->test_finished_event_.Signal();
        }

        base::TimeDelta dhcp_delay_;
        base::TimeDelta timeout_;
        std::string configured_url_;
        int fetcher_delay_ms_;
        int fetcher_result_;
        std::string pac_script_;
        MockProxyScriptFetcher* fetcher_;
        base::OneShotTimer fetcher_timer_;
        scoped_refptr<DelayingDhcpQuery> dhcp_query_;
    };

    class FetcherClient {
    public:
        FetcherClient()
            : url_request_context_(new TestURLRequestContext())
            , worker_pool_(
                  new base::SequencedWorkerPool(4, "DhcpAdapterFetcherTest"))
            , fetcher_(new MockDhcpProxyScriptAdapterFetcher(
                  url_request_context_.get(),
                  worker_pool_->GetTaskRunnerWithShutdownBehavior(
                      base::SequencedWorkerPool::CONTINUE_ON_SHUTDOWN)))
        {
        }

        ~FetcherClient()
        {
            worker_pool_->Shutdown();
        }

        void WaitForResult(int expected_error)
        {
            EXPECT_EQ(expected_error, callback_.WaitForResult());
        }

        void RunTest()
        {
            fetcher_->Fetch("adapter name", callback_.callback());
        }

        void FinishTestAllowCleanup()
        {
            fetcher_->FinishTest();
            base::RunLoop().RunUntilIdle();
        }

        TestCompletionCallback callback_;
        std::unique_ptr<URLRequestContext> url_request_context_;
        scoped_refptr<base::SequencedWorkerPool> worker_pool_;
        std::unique_ptr<MockDhcpProxyScriptAdapterFetcher> fetcher_;
        base::string16 pac_text_;
    };

    TEST(DhcpProxyScriptAdapterFetcher, NormalCaseURLNotInDhcp)
    {
        FetcherClient client;
        client.fetcher_->configured_url_ = "";
        client.RunTest();
        client.WaitForResult(ERR_PAC_NOT_IN_DHCP);
        ASSERT_TRUE(client.fetcher_->DidFinish());
        EXPECT_EQ(ERR_PAC_NOT_IN_DHCP, client.fetcher_->GetResult());
        EXPECT_EQ(base::string16(L""), client.fetcher_->GetPacScript());
    }

    TEST(DhcpProxyScriptAdapterFetcher, NormalCaseURLInDhcp)
    {
        FetcherClient client;
        client.RunTest();
        client.WaitForResult(OK);
        ASSERT_TRUE(client.fetcher_->DidFinish());
        EXPECT_EQ(OK, client.fetcher_->GetResult());
        EXPECT_EQ(base::string16(L"bingo"), client.fetcher_->GetPacScript());
        EXPECT_EQ(GURL(kPacUrl), client.fetcher_->GetPacURL());
    }

    TEST(DhcpProxyScriptAdapterFetcher, TimeoutDuringDhcp)
    {
        // Does a Fetch() with a long enough delay on accessing DHCP that the
        // fetcher should time out.  This is to test a case manual testing found,
        // where under certain circumstances (e.g. adapter enabled for DHCP and
        // needs to retrieve its configuration from DHCP, but no DHCP server
        // present on the network) accessing DHCP can take on the order of tens
        // of seconds.
        FetcherClient client;
        client.fetcher_->dhcp_delay_ = TestTimeouts::action_max_timeout();
        client.fetcher_->timeout_ = base::TimeDelta::FromMilliseconds(25);

        base::ElapsedTimer timer;
        client.RunTest();
        // An error different from this would be received if the timeout didn't
        // kick in.
        client.WaitForResult(ERR_TIMED_OUT);

        ASSERT_TRUE(client.fetcher_->DidFinish());
        EXPECT_EQ(ERR_TIMED_OUT, client.fetcher_->GetResult());
        EXPECT_EQ(base::string16(L""), client.fetcher_->GetPacScript());
        EXPECT_EQ(GURL(), client.fetcher_->GetPacURL());
        client.FinishTestAllowCleanup();
    }

    TEST(DhcpProxyScriptAdapterFetcher, CancelWhileDhcp)
    {
        FetcherClient client;
        client.RunTest();
        client.fetcher_->Cancel();
        base::RunLoop().RunUntilIdle();
        ASSERT_FALSE(client.fetcher_->DidFinish());
        ASSERT_TRUE(client.fetcher_->WasCancelled());
        EXPECT_EQ(ERR_ABORTED, client.fetcher_->GetResult());
        EXPECT_EQ(base::string16(L""), client.fetcher_->GetPacScript());
        EXPECT_EQ(GURL(), client.fetcher_->GetPacURL());
        client.FinishTestAllowCleanup();
    }

    TEST(DhcpProxyScriptAdapterFetcher, CancelWhileFetcher)
    {
        FetcherClient client;
        // This causes the mock fetcher not to pretend the
        // fetcher finishes after a timeout.
        client.fetcher_->fetcher_delay_ms_ = -1;
        client.RunTest();
        int max_loops = 4;
        while (!client.fetcher_->IsWaitingForFetcher() && max_loops--) {
            base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(10));
            base::RunLoop().RunUntilIdle();
        }
        client.fetcher_->Cancel();
        base::RunLoop().RunUntilIdle();
        ASSERT_FALSE(client.fetcher_->DidFinish());
        ASSERT_TRUE(client.fetcher_->WasCancelled());
        EXPECT_EQ(ERR_ABORTED, client.fetcher_->GetResult());
        EXPECT_EQ(base::string16(L""), client.fetcher_->GetPacScript());
        // GetPacURL() still returns the URL fetched in this case.
        EXPECT_EQ(GURL(kPacUrl), client.fetcher_->GetPacURL());
        client.FinishTestAllowCleanup();
    }

    TEST(DhcpProxyScriptAdapterFetcher, CancelAtCompletion)
    {
        FetcherClient client;
        client.RunTest();
        client.WaitForResult(OK);
        client.fetcher_->Cancel();
        // Canceling after you're done should have no effect, so these
        // are identical expectations to the NormalCaseURLInDhcp test.
        ASSERT_TRUE(client.fetcher_->DidFinish());
        EXPECT_EQ(OK, client.fetcher_->GetResult());
        EXPECT_EQ(base::string16(L"bingo"), client.fetcher_->GetPacScript());
        EXPECT_EQ(GURL(kPacUrl), client.fetcher_->GetPacURL());
        client.FinishTestAllowCleanup();
    }

    // Does a real fetch on a mock DHCP configuration.
    class MockDhcpRealFetchProxyScriptAdapterFetcher
        : public MockDhcpProxyScriptAdapterFetcher {
    public:
        explicit MockDhcpRealFetchProxyScriptAdapterFetcher(
            URLRequestContext* context,
            scoped_refptr<base::TaskRunner> task_runner)
            : MockDhcpProxyScriptAdapterFetcher(context, task_runner)
            , url_request_context_(context)
        {
        }

        // Returns a real proxy script fetcher.
        ProxyScriptFetcher* ImplCreateScriptFetcher() override
        {
            ProxyScriptFetcher* fetcher = new ProxyScriptFetcherImpl(url_request_context_);
            return fetcher;
        }

        URLRequestContext* url_request_context_;
    };

    TEST(DhcpProxyScriptAdapterFetcher, MockDhcpRealFetch)
    {
        EmbeddedTestServer test_server;
        test_server.ServeFilesFromSourceDirectory(
            "net/data/proxy_script_fetcher_unittest");
        ASSERT_TRUE(test_server.Start());

        GURL configured_url = test_server.GetURL("/downloadable.pac");

        FetcherClient client;
        TestURLRequestContext url_request_context;
        scoped_refptr<base::TaskRunner> runner = client.worker_pool_->GetTaskRunnerWithShutdownBehavior(
            base::SequencedWorkerPool::CONTINUE_ON_SHUTDOWN);
        client.fetcher_.reset(
            new MockDhcpRealFetchProxyScriptAdapterFetcher(
                &url_request_context, runner));
        client.fetcher_->configured_url_ = configured_url.spec();
        client.RunTest();
        client.WaitForResult(OK);
        ASSERT_TRUE(client.fetcher_->DidFinish());
        EXPECT_EQ(OK, client.fetcher_->GetResult());
        EXPECT_EQ(base::string16(L"-downloadable.pac-\n"),
            client.fetcher_->GetPacScript());
        EXPECT_EQ(configured_url,
            client.fetcher_->GetPacURL());
    }

#define BASE_URL "http://corpserver/proxy.pac"

    TEST(DhcpProxyScriptAdapterFetcher, SanitizeDhcpApiString)
    {
        const size_t kBaseUrlLen = strlen(BASE_URL);

        // Default case.
        EXPECT_EQ(BASE_URL,
            DhcpProxyScriptAdapterFetcher::SanitizeDhcpApiString(
                BASE_URL, kBaseUrlLen));

        // Trailing \n and no null-termination.
        EXPECT_EQ(BASE_URL,
            DhcpProxyScriptAdapterFetcher::SanitizeDhcpApiString(
                BASE_URL "\nblablabla", kBaseUrlLen + 1));

        // Embedded NULLs.
        EXPECT_EQ(BASE_URL,
            DhcpProxyScriptAdapterFetcher::SanitizeDhcpApiString(
                BASE_URL "\0foo\0blat", kBaseUrlLen + 9));
    }

#undef BASE_URL

} // namespace

} // namespace net
